Skip to content

feat: gif support for profile picture#1366

Open
TakuyaYagam1 wants to merge 13 commits into
caelestia-dots:mainfrom
TakuyaYagam1:main
Open

feat: gif support for profile picture#1366
TakuyaYagam1 wants to merge 13 commits into
caelestia-dots:mainfrom
TakuyaYagam1:main

Conversation

@TakuyaYagam1
Copy link
Copy Markdown

Summary

  • Add animated GIF support for profile pictures (~/.face)
  • New ProfileImage component that detects image format via magic bytes (QImageReader) and switches between AnimatedImage (GIF) and CachingImage (static)
  • GIF added to file picker filter so users can select .gif files directly

Changes

  • CUtils::imageFormat() — new C++ method for format detection without file extension
  • components/images/ProfileImage.qml — Loader-based component switching between animated/static rendering
  • Updated User.qml (dashboard) and Center.qml (lock screen) to use ProfileImage
  • Added gif to valid image types in Images.qml
gif.mp4

Purple squares are an unrelated icon loading issue, not caused by this PR.

Copilot AI review requested due to automatic review settings April 1, 2026 19:35
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds animated GIF support for profile pictures (~/.face) by introducing a format-detecting ProfileImage component that switches between animated and static rendering.

Changes:

  • Add gif to supported image types/extensions for the file picker.
  • Introduce CUtils::imageFormat() (QImageReader-based) for extension-independent format detection.
  • Replace CachingImage usage with ProfileImage in dashboard and lock screen profile-picture views.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
utils/Images.qml Extends allowed image extensions/types to include GIF.
plugin/src/Caelestia/cutils.hpp Exposes new QML-invokable imageFormat() helper.
plugin/src/Caelestia/cutils.cpp Implements format detection via QImageReader::imageFormat().
modules/lock/Center.qml Switches lock-screen avatar rendering to ProfileImage.
modules/dashboard/dash/User.qml Switches dashboard avatar to ProfileImage and triggers reload after picker accept.
components/images/ProfileImage.qml New loader-based component selecting AnimatedImage vs CachingImage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +12 to +18

function reload(): void {
_format = CUtils.imageFormat(path);
loader.active = false;
loader.active = true;
}

Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reload() assigns to _format, which removes the existing QML binding (property string _format: CUtils.imageFormat(path)). After the first reload, _format will no longer update automatically when path changes, so the component can get stuck using the wrong renderer. Consider keeping _format as a binding (e.g., reassign via Qt.binding(...)) or refactor so reload forces a reload without breaking the binding.

Copilot uses AI. Check for mistakes.
Comment thread components/images/ProfileImage.qml Outdated
fillMode: AnimatedImage.PreserveAspectCrop
asynchronous: true
playing: true
source: `file://${root.path}`
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnimatedImage.source is built as file://${root.path}, which can produce malformed file URLs for absolute paths (e.g. file:////home/...) and won’t URL-escape characters like spaces. Elsewhere in the codebase AnimatedImage.source is set to a plain local path string (e.g. Paths.absolutePath(...)), which avoids these issues. Prefer using the local path directly (or a proper file-URL constructor) here.

Suggested change
source: `file://${root.path}`
source: root.path

Copilot uses AI. Check for mistakes.
@TakuyaYagam1
Copy link
Copy Markdown
Author

CI failures are pre-existing and unrelated to this PR:

  • No PanelWindow backend loaded - Wayland backend unavailable in CI
  • Member "status" not found on type "QObject" - qmllint limitation with Loader.item type resolution
  • Unused import qs.utils in Center.qml - false positive, Paths.home is used from it

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

property string path
property string _format: CUtils.imageFormat(path)

readonly property int status: (loader.item as Image)?.status ?? Image.Null
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

status is derived via (loader.item as Image)?.status, but the loaded item can be an AnimatedImage (not an Image), causing the cast to fail and status to stay Image.Null for GIFs (placeholder will never hide). Use loader.item?.status ?? Image.Null (optionally suppress qmllint like other image wrappers do).

Suggested change
readonly property int status: (loader.item as Image)?.status ?? Image.Null
readonly property int status: loader.item?.status ?? Image.Null

Copilot uses AI. Check for mistakes.
Comment thread components/images/ProfileImage.qml Outdated
fillMode: AnimatedImage.PreserveAspectCrop
asynchronous: true
playing: true
source: Qt.resolvedUrl(`file://${root.path}`)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The AnimatedImage source URL is built as file://${root.path}, which produces malformed URLs for normal absolute paths (e.g., /home/... becomes file:////home/...) and can also mis-handle relative paths (treated as a host). Prefer generating a proper local-file URL (e.g., resolve the local path directly, or use a helper that produces file:///...).

Suggested change
source: Qt.resolvedUrl(`file://${root.path}`)
source: Qt.resolvedUrl(root.path)

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +43
AnimatedImage {
fillMode: AnimatedImage.PreserveAspectCrop
asynchronous: true
playing: true
source: Qt.resolvedUrl(`file://${root.path}`)
}
}

Component {
id: cachingComponent

CachingImage {
path: root.path
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loaded AnimatedImage/CachingImage items are not sized to the Loader (no anchors.fill: parent / width+height binding), so the profile image may render at its implicit size instead of filling the avatar container. Add sizing/anchors inside both components so they always fill root like the previous direct CachingImage usage did.

Copilot uses AI. Check for mistakes.
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;
Q_INVOKABLE bool deleteFile(const QUrl& path) const;
Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
Q_INVOKABLE QString imageFormat(const QString& path) const;
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New CUtils::imageFormat takes a raw QString path, unlike the other filesystem-related helpers here which take QUrl and validate isLocalFile(). Consider switching this API to QUrl (and rejecting non-local URLs) so QML call sites are consistent and you don’t accidentally pass a qrc://remote URL string that will always return an empty format.

Suggested change
Q_INVOKABLE QString imageFormat(const QString& path) const;
Q_INVOKABLE QString imageFormat(const QUrl& url) const;
QString imageFormat(const QString& path) const;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread components/images/ProfileImage.qml Outdated
fillMode: AnimatedImage.PreserveAspectCrop
asynchronous: true
playing: true
source: Qt.resolvedUrl(`file://${root.path}`)
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnimatedImage.source is built via Qt.resolvedUrl(file://${root.path}), which can produce non-canonical/invalid file URLs for absolute paths (e.g., file:////home/...) and differs from existing AnimatedImage usage in the repo (which passes an absolute local path string). Prefer using Qt.resolvedUrl(root.path) (no manual file:// prefix) or pass root.path directly to let QML convert the local path to a URL correctly.

Suggested change
source: Qt.resolvedUrl(`file://${root.path}`)
source: Qt.resolvedUrl(root.path)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread utils/Images.qml
Comment on lines +6 to +7
readonly property list<string> validImageTypes: ["jpeg", "png", "webp", "tiff", "svg", "gif"]
readonly property list<string> validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg", "gif"]
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding gif to the global validImageExtensions affects every FileDialog using this list (e.g., wallpaper picker) and will allow selecting GIFs even where only static rendering is supported. Consider scoping this to profile-picture selection (separate list) or ensuring all consumers can handle GIFs appropriately.

Copilot uses AI. Check for mistakes.
@TakuyaYagam1 TakuyaYagam1 force-pushed the main branch 2 times, most recently from 63b6188 to 93d8368 Compare April 14, 2026 20:13
@TakuyaYagam1 TakuyaYagam1 force-pushed the main branch 4 times, most recently from 13d7d2b to 0148a73 Compare April 29, 2026 19:59
@TakuyaYagam1
Copy link
Copy Markdown
Author

Quick update: I rebased the PR on top of the latest upstream changes after the new image cache provider merge.

I tested an additional follow-up that moved image format detection into Caelestia.Images.IUtils and disabled QML memory cache for profile images, but that version regressed GIF loading on my NixOS setup, so I rolled it back for now.

The current branch keeps the stable behavior:

  • GIF profile pictures use AnimatedImage, preserving animation.
  • Static profile pictures continue to use the upstream CachingImage path.
  • Global CachingImage behavior is unchanged.

I’ll keep the PR focused on the working GIF support first, and can revisit deeper integration with the new image utils/cache provider separately if needed.

@TakuyaYagam1
Copy link
Copy Markdown
Author

Updated this PR after the upstream image cache provider changes.

The current version keeps GIF profile pictures on AnimatedImage, so animation is preserved and GIFs do not go through the static image cache provider. Static profile pictures still use the upstream CachingImage path.

Follow-up polish included:

  • disabled QML memory cache only for profile images, so replacing ~/.face updates reliably;
  • deferred dashboard profile image reload after picker accept;
  • made image extension checks case-insensitive for .GIF, .PNG, .JPG, etc.

I intentionally avoided changing global CachingImage behavior and left MP4/WebM support out of scope for this PR.

Tested on my NixOS setup by rebuilding Caelestia Shell and selecting/replacing GIF profile pictures. Everything works as expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants